﻿using Drive.AlertSystem;
using Drive.DbAccess;
using Drive.LiveExcelHost.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading.Tasks;
using System.Timers;

namespace Drive.LiveExcelHost.Services
{
    class ExcelService : SocketIOService
    {
        private string fileName;
        private string filePath;

        internal static ConcurrentDictionary<string, Spreadsheet> spreadsheets = new ConcurrentDictionary<string, Spreadsheet>();
        public ExcelService(Request request, WebSocketSession session) : base(request, session)
        {
            this.fileName = request.Parameters["guid"];
            this.filePath = Path.Combine(AppContext.BaseDirectory, "spreadsheets", fileName + ".json");
            this.BindDocument();
            DelayTicker.AddHandler(DelayTicker.T2S, this.OnTimerTick);
        }

        public static Spreadsheet LoadSpreadsheet(string fileName)
        {
            if (!spreadsheets.TryGetValue(fileName, out var spreadsheet))
            {
                var path1 = Path.Combine(AppContext.BaseDirectory, "dbspreadsheets", fileName + ".json");
                if (File.Exists(path1))
                {
                    var str = File.ReadAllText(path1);
                    var dbsql = JsonConvert.DeserializeObject<DbSql>(str);
                    if (dbsql.ExpireAt < DateTime.Now)
                    {
                        spreadsheet = Spreadsheet.Error("提示", "查询定义已过期, 如有疑问请联系管理员");
                    }
                    else
                    {
                        var jdbc = JdbcString.Parse(dbsql.Jdbc);
                        jdbc.UserName = dbsql.Username;
                        jdbc.Password = dbsql.Password;
                        IDbInterface db = jdbc.GetDbInterface();
                        var dt = db.ExecuteDataTable(dbsql.Sql);
                        var ss = Spreadsheet.FromDataTable(dt, true, true);
                        if (spreadsheets.TryAdd(fileName, ss))
                            spreadsheet = ss;
                    }
                }
                else
                {
                    var path = Path.Combine(AppContext.BaseDirectory, "spreadsheets", fileName + ".json");
                    if (!File.Exists(path))
                    {
                        File.WriteAllText(path, "[{\"columns\":[{\"type\":\"text\",\"align\":\"center\",\"width\":\"212\",\"source\":[],\"options\":[]},{\"type\":\"text\",\"align\":\"center\",\"width\":\"182.60000228881836\",\"source\":[],\"options\":[]}],\"minDimensions\":[2,1],\"worksheetName\":\"Sheet1\",\"data\":[{\"row\":0,\"data\":[\"hello\",\"world\"]}]}]");
                    }
                    var json = File.ReadAllText(path, Encoding.UTF8);
                    var ss = JsonConvert.DeserializeObject<Spreadsheet>(json);
                    if (spreadsheets.TryAdd(fileName, ss))
                        spreadsheet = ss;
                }
            }
            else if (spreadsheet.DoNotCache)
            {
                if (spreadsheets.TryRemove(fileName, out spreadsheet))
                {
                    return LoadSpreadsheet(fileName);
                }
            }
            return spreadsheet;
        }
        public void BindDocument()
        {
            var fileName = request.Parameters["guid"];

            this.spreadsheet = LoadSpreadsheet(fileName);
        }

        private async void OnTimerTick(double obj)
        {
            if (LastBorderAction != null)
            {
                await RedisPubSub.Publish($"Excel{this.fileName}", LastBorderAction);
                LastBorderAction = null;
                Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} {this.session.SessionId} PUBL SELF PACKET OK");
            }
        }
        private const string cmdResetBorders = "{\r\n  \"0\": \"resetBorders\"\r\n}";

        private async Task ResetBorders()
        {
            var body = new Newtonsoft.Json.Linq.JObject();
            body.Add("u", this.session.SessionId);
            body.Add("a", JObject.Parse(cmdResetBorders));
            var jexcel = new Newtonsoft.Json.Linq.JArray();
            jexcel.Add("jexcel");
            jexcel.Add(body);
            var d = jexcel.ToString(Newtonsoft.Json.Formatting.None);
            await RedisPubSub.Publish($"Excel{this.fileName}", d);
        }

        /// <summary>
        /// 收到自己的消息
        /// </summary>
        /// <param name="content"></param>
        /// <returns></returns>
        public override async Task OnMessageReceived(string type, string content)
        {
            var body = new Newtonsoft.Json.Linq.JObject();
            body.Add("u", this.session.SessionId);
            body.Add("a", Newtonsoft.Json.Linq.JArray.Parse(content)[1]);
            var jexcel = new Newtonsoft.Json.Linq.JArray();
            jexcel.Add("jexcel");
            jexcel.Add(body);
            var d = jexcel.ToString(Newtonsoft.Json.Formatting.None);
            Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} {this.session.SessionId} RECV SELF PACKET: {content}");

            var action = body["a"]["0"].ToString();
            if (realtimeActions.Any(x => x == action))
            {
                //发送给全部人
                await RedisPubSub.Publish($"Excel{this.fileName}", d);
                Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} {this.session.SessionId} PUBL SELF PACKET OK");
            }
            else if (action == "setBorder" || action == "resetBorders")
            {
                LastBorderAction = d;
            }
            try
            {
                this.HandleSpreadUpdate(body["a"] as JObject);
            }
            catch (Exception ex)
            {
                this.ReportError(ex);
            }
        }

        internal override async Task OnSocketIoSessionCreate()
        {
            await RedisPubSub.Subscribe($"Excel{this.fileName}", this.OnMessage);
            Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} {this.session.SessionId} SUBSCRIB OK ============================= ");

            var toSend = TextUtil.TextToBytes("6");
            await session.WebSocket.SendAsync(toSend, WebSocketMessageType.Text, true, session.cancellationToken);
            await base.OnSocketIoSessionCreate();
        }

        /// <summary>
        /// 收到别人的消息
        /// </summary>
        /// <param name="channel"></param>
        /// <param name="message"></param>
        private async void OnMessage(RedisChannel channel, RedisValue message)
        {
            var sessionId = Newtonsoft.Json.Linq.JArray.Parse(message)[1]["u"].ToString();
            if (sessionId != this.session.SessionId)
            {
                Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} RECV {this.session.SessionId} <== {sessionId}: {message}");
                var content = TextUtil.TextToBytes("42" + message);
                if (session.WebSocket.State == System.Net.WebSockets.WebSocketState.Open)
                {
                    try
                    {
                        await this.session.WebSocket.SendAsync(content, System.Net.WebSockets.WebSocketMessageType.Text, true, session.cancellationToken);
                        Console.WriteLine($"{DateTime.Now.ToString("mm:ss")} SEND {this.session.SessionId} ==> browser: OK");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"ERR {DateTime.Now.ToString("mm:ss")} SEND {this.session.SessionId} ==> browser: {ex.Message}");
                        OnSendError(ex);
                        await this.OnSocketIoSessionDestory();
                    }
                }
            }
        }

        private void OnSendError(Exception ex)
        {
            var sid = this.request.Cookies.ContainsKey("sid") ? this.request.Cookies["sid"] : "";
            var builder = EventBuilder.Create("LiveHost", this.GetType().Name, ex.Message)
                        .SetError("ERR_DELIVER_MSG_TO_WEBSOCKET", $"SessionId={this.session.SessionId} WebSockektState = {this.session.WebSocket.State}")
                        .SetLogLevel(NLog.LogLevel.Info);

            NLog.LogManager.GetLogger(this.GetType().Name).Log(builder.Build());
        }
        private string LastBorderAction = null;

        private static string[] realtimeActions = new string[] {
            "setWidth",
            "setValue",
            "orderBy",
            "insertColumn",
            "insertRow",
            "deleteColumn",
            "deleteRow",
            "setHeader",
            "setRowId"
        };

        private void HandleSpreadUpdate(JObject d)
        {
            var action = d["0"].ToString();
            if (action == "setWidth")
            {
                var wTo = d["2"].Value<int>();
                if (d["1"].HasValues)
                {
                    foreach (var col in d["1"].Values<int>())
                    {
                        var to = d["2"].Value<int>();
                        if (spreadsheet[0].columns.Count < col)
                        {
                            var adding = spreadsheet[0].minDimensions[1] - spreadsheet[0].columns.Count;
                            spreadsheet[0].columns.AddRange(Enumerable.Repeat(new Column
                            {
                                align = "center",
                                options = new object[0],
                                source = new object[0],
                                type = "text",
                                width = 50
                            }, adding));
                        }
                        spreadsheet[0].columns[col].width = wTo;
                    }
                }
                else
                {
                    var col = d["1"].Value<int>();
                    var wFrom = d["3"].Value<int>();
                    if (spreadsheet[0].columns.Count < col)
                    {
                        var adding = Math.Max(col + 1, spreadsheet[0].minDimensions[1]) - spreadsheet[0].columns.Count;
                        spreadsheet[0].columns.AddRange(Enumerable.Repeat(new Column
                        {
                            align = "center",
                            options = new object[0],
                            source = new object[0],
                            type = "text",
                            width = 50
                        }, adding));
                    }
                    spreadsheet[0].columns[col].width = wTo;
                }
            }
            else if (action == "setValue")
            {
                foreach (Newtonsoft.Json.Linq.JObject item in d["1"])
                {
                    var col = item["col"].Value<int>();
                    var row = item["row"].Value<int>();
                    var newValue = item["newValue"].Value<string>();
                    var dataRow = spreadsheet[0].data.FirstOrDefault(x => x.row == row);
                    if (dataRow == null)
                    {
                        int columnsMax = spreadsheet[0].data.Max(x => x.data.Count);
                        dataRow = new Datum(columnsMax) { row = row };
                        spreadsheet[0].data.Add(dataRow);
                    }
                    if (dataRow.data.Count <= col)
                    {
                        dataRow.data.AddRange(Enumerable.Repeat((string)null, col - dataRow.data.Count + 1));
                    }
                    foreach (var dd in spreadsheet[0].data)
                    {
                        dd.row = spreadsheet[0].data.IndexOf(dd);
                    }
                    dataRow.data[col] = newValue;
                }
            }
            else if (action == "orderBy")
            {
                var index = d["1"].Value<int>();
                var desc = d["2"].Value<int>();
                var rowIdxs = d["3"].Values<int>();

                var elementIdx = 0;
                for (int i = 0; i < rowIdxs.Count(); i++)
                {
                    var sort = rowIdxs.ElementAt(i);
                    var item = spreadsheet[0].data.Find(x => x.row == sort);
                    spreadsheet[0].data.Remove(item);
                    spreadsheet[0].data.Insert(elementIdx, item);
                    elementIdx++;
                }
                foreach (var item in spreadsheet[0].data)
                {
                    item.row = spreadsheet[0].data.IndexOf(item);
                }
            }
            else if (action == "insertColumn")
            {
                var count = d["1"].Value<int>();
                var index = d["2"].Value<int>();
                var before = d["3"].Value<bool>();
                var columns = d["4"].ToObject<List<Column>>();
                var value = d["5"].Value<string>();

                if (spreadsheet[0].columns.Count <= index)
                {
                    var cols = Math.Max(index + 1, spreadsheet[0].minDimensions[0]);
                    spreadsheet[0].columns.AddRange(
                        Enumerable.Repeat(new Column
                        {
                            align = "center",
                            options = new object[0],
                            source = new object[0],
                            type = "text",
                            width = 50
                        }, cols - spreadsheet[0].columns.Count));
                }
                spreadsheet[0].columns.InsertRange(before ? index : index + 1, Enumerable.Repeat(new Column
                {
                    align = "center",
                    options = new object[0],
                    source = new object[0],
                    type = "text",
                    width = 50
                }, count));
            }
            else if (action == "insertRow")
            {
                var rows = d["1"].Value<int>();
                var idx = d["2"].Value<int>();
                var flag = d["3"].Value<bool>();

                List<Datum> items = new List<Datum>();
                foreach (var item in d["4"])
                {
                    var datum = item.ToObject<Datum>();
                    items.Add(datum);
                }
                int columnsMax = spreadsheet[0].data.Max(x => x.data.Count);
                var insertAt = flag ? idx : idx + 1;
                if (spreadsheet[0].data.Count <= insertAt)
                {
                    var adding = Math.Max(insertAt + 1, spreadsheet[0].minDimensions[1]);
                    spreadsheet[0].data.AddRange(Enumerable.Repeat(new Datum(columnsMax), adding - spreadsheet[0].data.Count));
                }
                spreadsheet[0].data.InsertRange(insertAt, items);
                foreach (var item in spreadsheet[0].data)
                {
                    item.row = spreadsheet[0].data.IndexOf(item);
                }
            }
            else if (action == "deleteRow")
            {
                var index = d["1"].Value<int>();
                var count = d["2"].Value<int>();
                for (int i = 0; i < count; i++)
                {
                    if (index < spreadsheet[0].data.Count)
                    {
                        spreadsheet[0].data.RemoveAt(index);
                    }
                }

                foreach (var item in spreadsheet[0].data)
                {
                    item.row = spreadsheet[0].data.IndexOf(item);
                }
            }
            else if (action == "deleteColumn")
            {
                var index = d["1"].Value<int>();
                var count = d["2"].Value<int>();
                for (int i = 0; i < count; i++)
                {
                    if (index < spreadsheet[0].columns.Count)
                    {
                        spreadsheet[0].columns.RemoveAt(index);
                    }
                }

                foreach (var item in spreadsheet[0].data)
                {
                    for (int i = 0; i < count; i++)
                    {
                        if (index < item.data.Count)
                        {
                            item.data.RemoveAt(index);
                        }
                    }
                }
            }
            else if (action == "setHeader")
            {
                var index = d["1"].Value<int>();
                var title = d["2"].Value<string>();
                spreadsheet[0].columns[index].title = title;
            }
            else if (action == "setRowId")
            {
                foreach (var item in d["1"])
                {
                    var index = item["y"].Value<int>();
                    var rowId = item["id"].Value<string>();
                    if (index >= spreadsheet[0].data.Count)
                    {
                        var cols = Math.Max(index + 1, spreadsheet[0].minDimensions[0]);
                        spreadsheet[0].data.AddRange(Enumerable.Repeat(new Datum(cols), cols - spreadsheet[0].data.Count));
                    }
                    spreadsheet[0].data[index].id = rowId;
                }
            }
            else if (action == "setBorder")
            {
                return;
            }
            else if (action == "resetBorders")
            {
                return;
            }
            else if (action == "undo")
            {
                HandleUndoAction(d);
                return;
            }
            else if (action == "redo")
            {
                HandleRedoAction(d);
                return;
            }
            var json = JsonConvert.SerializeObject(spreadsheet, Newtonsoft.Json.Formatting.None);
            if (!spreadsheet.ReadOnly)
                System.IO.File.WriteAllText(filePath, json);

        }

        private void HandleRedoAction(JObject d)
        {
        }

        private void HandleUndoAction(JObject d)
        {
            var undo = d["1"]["action"].Value<string>();
            if (undo == "setValue")
            {
                var records = d["1"]["records"];
                foreach (var item in records)
                {
                }
                var selection = d["1"]["selection"];
            }
            else if (undo == "insertRow")
            {

            }
            else if (undo == "insertColumn")
            {

            }
        }
        public Spreadsheet spreadsheet;

        internal override async Task OnSocketIoSessionDestory()
        {
            await ResetBorders();
            Console.WriteLine($"INFO {DateTime.Now.ToString("mm:ss")} {this.session.SessionId} SESSION LEAVE ");
            await RedisPubSub.Unsubscribe($"Excel{this.fileName}", this.OnMessage);
            await base.OnSocketIoSessionDestory();
        }
    }

    class DelayTicker
    {
        public static Timer T2S = new Timer(2000);
        static Dictionary<Timer, List<Action<double>>> handlers = new Dictionary<Timer, List<Action<double>>>();
        static DelayTicker()
        {
            T2S.Elapsed += T3S_Elapsed;
            T2S.Start();
        }

        public static void AddHandler(Timer timer, Action<double> handler)
        {
            lock (handlers)
            {
                if (handlers.ContainsKey(timer))
                {
                    handlers[timer].Add(handler);
                }
                else
                {
                    handlers.Add(timer, new List<Action<double>> { handler });
                }
            }
        }

        private static void T3S_Elapsed(object sender, ElapsedEventArgs e)
        {
            List<Action<double>> items = new List<Action<double>>();
            var inst = sender as Timer;
            lock (handlers)
            {
                items.AddRange(handlers[inst]);
            }
            foreach (var item in items)
            {
                item(inst.Interval);
            }
        }
    }
}
